/*
 * Copyright (c) 2011, Marc-Andre Ferland - madrang@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the The Warrent Team nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *     * This software is free for non-commercial use. You may not use this
 *       software, in whole or in part, in support of any commercial product
 *       without the express consent of the author.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE WARRENT TEAM BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Runtime.InteropServices;

//using OsHelper.UsbNet.LibUsb.Descriptors;

namespace OsHelper.UsbNet.LibUsb
{
	/// <summary>
    /// Representing a USB device that can be opened and used by Libusb-1.0.
    /// </summary> 
    public class Profile : OsHelper.Handles.Handle
	{
		/// <summary>
        /// Wraps a raw usb device profile handle pointer in a <see cref="Handle"/> class.
        /// </summary>
        /// <param name="pProfileHandle">the profile handle to wrap.</param>
		public Profile(IntPtr ProfileHandle) : base()
		{
			this.SetHandle(Profile.RefDevice(ProfileHandle));
			
			this.DeviceDescriptor = this.GetDeviceDescriptor();
		}
		
		public Profile (Device MyDevice) : base()
		{
			this.SetHandle(Profile.RefDevice(Profile.GetProfile (MyDevice)));
			
			this.DeviceDescriptor = this.GetDeviceDescriptor();
		}
		
		/// <summary>
        /// When overridden in a derived class, executes the code required to free the handle.
        /// </summary>
        /// <returns>
        /// true if the handle is released successfully; otherwise, in the event of a catastrophic failure, false.
        /// </returns>
		protected override bool ReleaseHandle()
		{
			if (!this.IsInvalid)
			{
				Profile.UnrefDevice(this.handle);
				this.SetHandleAsInvalid();
			}
			return true;
		}
		
		#region Properties
		public byte BusNumber
		{
			get { return Profile.GetBusNumber(this); }
		}
		public byte DeviceAddress
		{
			get { return Profile.GetDeviceAddress(this); }
		}
		
		/// <summary>
        /// Gets the standard usb device descriptor.
        /// </summary>
		public OsHelper.UsbNet.Descriptors.DeviceDescriptor DeviceDescriptor {
			get;
			private set;
		}
		#endregion
		
		#region Method
		
		/// <summary>
        /// Compares a <see cref="Profile"/> with this one.
        /// </summary>
        /// <param name="other">The other <see cref="Profile"/>.</param>
        /// <returns>True if the <see cref="BusNumber"/> and <see cref="DeviceAddress"/> are equal.</returns>
        public bool Equals(Profile other)
        {
            if (ReferenceEquals(null, other)) return false;
            if (ReferenceEquals(this, other)) return true;
            return this.BusNumber == other.BusNumber && this.DeviceAddress == other.DeviceAddress;
        }
		
		/// <summary>
        /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
        /// </summary>
        /// <returns>
        /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
        /// </returns>
		public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != typeof (Profile)) return false;
            return Equals((Profile)obj);
        }
		
		///<summary>
        /// <c>true</c> if the <see cref="Profile"/> types are equal.
        ///</summary>
        /// <remarks>
        /// <see cref="Profile"/> types are considered equal they have the same <see cref="BusNumber"/> and <see cref="DeviceAddress"/>.
        /// </remarks>
        ///<param name="left"><see cref="Profile"/> on the left.</param>
        ///<param name="right"><see cref="Profile"/> on the right.</param>
        ///<returns>True if the <see cref="Profile"/> types are equal.</returns>
        public static bool operator ==(Profile left, Profile right)
		{
			return Equals(left, right);
		}

        ///<summary>
        /// <c>true</c> if the <see cref="Profile"/> types are not equal.
        ///</summary>
        /// <remarks>
        /// <see cref="Profile"/> types are considered equal they have the same <see cref="BusNumber"/> and <see cref="DeviceAddress"/>.
        /// </remarks>
        ///<param name="left"><see cref="Profile"/> on the left.</param>
        ///<param name="right"><see cref="Profile"/> on the right.</param>
        ///<returns>True if the <see cref="Profile"/> types are not equal.</returns>
        public static bool operator !=(Profile left, Profile right)
		{
			return !Equals(left, right);
		}
		
		/// <summary>
        /// Serves as a hash function for a particular type. 
        /// </summary>
        /// <returns>
        /// A hash code for the current <see cref="T:System.Object"/>.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        public override int GetHashCode()
        {
			return (this.BusNumber.GetHashCode() ^ this.DeviceAddress.GetHashCode());
        }
		
		public Profile Copy()
		{
			return new Profile(this.handle);
		}
		
		#endregion
		
		#region External Method
		
		/// <summary>
		/// Get the number of the bus that a device is connected to. 
		/// </summary>
		/// <remarks>
		/// <note type="tip" title="Libusb-1.0 API:"><seelibusb10 group="dev"/></note>
		/// </remarks>
		/// <returns>The bus number.</returns>
		/// <param name="deviceProfileHandle">A device profile handle.</param>
		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_get_bus_number")]
		private static extern byte GetBusNumber ([In] Profile deviceProfileHandle);

		/// <summary>
		/// Get the address of the device on the bus it is connected to. 
		/// </summary>
		/// <remarks>
		/// <note type="tip" title="Libusb-1.0 API:"><seelibusb10 group="dev"/></note>
		/// </remarks>
		/// <returns>The device address.</returns>
		/// <param name="deviceProfileHandle">A device profile handle.</param>
		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_get_device_address")]
		private static extern byte GetDeviceAddress ([In] Profile deviceProfileHandle);

		/*	
		/// <summary>
		/// Convenience function to retrieve the wMaxPacketSize value for a particular endpoint in the active device configuration. 
		/// </summary>
		/// <param name="deviceProfileHandle">A device profile handle.</param>
		/// <param name="endpoint">Endpoint address to retrieve the max packet size for.</param>
		/// <remarks>
		/// <note type="tip" title="Libusb-1.0 API:"><seelibusb10 group="dev"/></note>
		/// <para>This function was originally intended to be of assistance when setting up isochronous transfers, but a design mistake resulted in this function instead. It simply returns the <see cref="EndpointDescriptor.wMaxPacketSize"/> value without considering its contents. If you're dealing with isochronous transfers, you probably want <see cref="GetMaxIsoPacketSize"/> instead.</para>
		/// </remarks>
		/// <returns>The <see cref="EndpointDescriptor.wMaxPacketSize"/></returns>
		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_get_max_packet_size")]
		private static extern int GetMaxPacketSize ([In] Profile deviceProfileHandle, byte endpoint);

		/// <summary>
		/// Calculate the maximum packet size which a specific endpoint is capable is sending or receiving in the duration of 1 microframe.
		/// </summary>
		/// <remarks>
		/// <para>Only the active configuration is examined. The calculation is based on the wMaxPacketSize field in the endpoint descriptor as described in section 9.6.6 in the USB 2.0 specifications.</para>
		/// <para>If acting on an isochronous or interrupt endpoint, this function will multiply the value found in bits 0:10 by the number of transactions per microframe (determined by bits 11:12). Otherwise, this function just returns the numeric value found in bits 0:10.</para>
		/// <para>This function is useful for setting up isochronous transfers, for example you might pass the return value from this function to <see  cref="Transfer.SetIsoPacketLengths">libusb_set_iso_packet_lengths</see> in order to set the length field of every isochronous packet in a transfer.</para>
		/// <note type="tip" title="Libusb-1.0 API:"><seelibusb10 group="dev"/></note>
		/// </remarks>
		/// <param name="deviceProfileHandle">A device profile handle.</param>
		/// <param name="endpoint">Endpoint address to retrieve the max packet size for.</param>
		/// <returns>The maximum packet size which can be sent/received on this endpoint.</returns>
		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_get_max_iso_packet_size")]
		private static extern int GetMaxIsoPacketSize ([In] Profile deviceProfileHandle, byte endpoint);
		*/
		
		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_ref_device")]
		private static extern IntPtr RefDevice (IntPtr ProfileHandle);

		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_unref_device")]
		private static extern IntPtr UnrefDevice (IntPtr ProfileHandle);
		
        /// <summary>
        /// Get a <see cref="Profile Handle"/> for a <see cref="IntPtr"/>. 
        /// </summary>
        /// <remarks>
        /// <note type="tip" title="Libusb-1.0 API:"><seelibusb10 group="dev"/></note>
        /// </remarks>
        /// <param name="devicehandle">A device handle.</param>
        /// <returns>The underlying profile handle.</returns>
		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_get_device")]
		private static extern IntPtr GetProfile ([In] Device devicehandle);

		#endregion
		
		#region ProfileList
		
		/// <summary>
		/// This is your entry point into finding a USB device to operate.
		/// Contain a list of USB devices currently attached to the system. 
		/// </summary>
		public static List<Profile> GetProfileList (Session SessionHandle)
		{
			List<Profile> ProfileList = new List<Profile>();
			IntPtr ListHandle = IntPtr.Zero;
			int DeviceCount = 0;
			
			try {
				DeviceCount = Profile.GetDeviceList(SessionHandle, out ListHandle);
				
				for (int i = 0; i < DeviceCount; i++) {
					IntPtr ProfileHandle = Marshal.ReadIntPtr(new IntPtr(ListHandle.ToInt64() + (i * IntPtr.Size)));
					
					//TODO Should probably throw error for having an invalid device handle before reaching device count.
					if(ProfileHandle == IntPtr.Zero)
						continue;
				
					ProfileList.Add(new Profile(ProfileHandle));
				}
			} finally {
				Profile.FreeDeviceList(ListHandle, true);
			}
			
			return ProfileList;
		}
		
		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_get_device_list")]
		private static extern int GetDeviceList ([In] Session SessionHandle, [Out] out IntPtr ProfileList);
		
		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_free_device_list")]
		private static extern void FreeDeviceList (IntPtr ProfileList, bool unrefDevices);
		
		#endregion
		
		#region DeviceDescriptor
		
		private UsbNet.Descriptors.DeviceDescriptor GetDeviceDescriptor ()
		{
			byte[] TempData = new byte[UsbNet.Descriptors.DeviceDescriptor.Size];
			
			LibUsb.Error ErrorCode = Profile.GetDeviceDescriptor (this, TempData);
			
			switch (ErrorCode) {
			case Error.Success:
				return UsbNet.Descriptors.DeviceDescriptor.FromBytes (TempData);
			case Error.InvalidParameter:
				throw new ArgumentException ();
			case Error.AccessDenied:
				throw new UnauthorizedAccessException ("Insufficient permissions to access Usb port.");
			case Error.NoDevice:
				throw new DeviceNotFoundException ();
			case Error.Timeout:
				throw new TimeoutException ();
			default:
				throw new Exception ("Unknown error in DeviceDescriptor:GetDeviceDescriptor, LibUsb Code : " + ErrorCode);
			}
		}
		
		/// <summary>
		/// Gets the standard device descriptor.
		/// </summary>
		/// <remarks>
		/// <note type="tip">This is a non-blocking function which does not involve any requests being sent to the device.</note>
		/// <note type="tip" title="Libusb-1.0 API:"><seelibusb10 group="desc"/></note>
		/// </remarks>
		/// <param name="deviceProfileHandle">A device profile handle.</param>
		/// <param name="deviceDescriptor">The <see cref="DeviceDescriptor"/> clas that will hold the data.</param>
		/// <returns>0 on success or a <see cref="Error"/> code on failure.</returns>
		[DllImport("libusb-1.0.dll", SetLastError = false, EntryPoint = "libusb_get_device_descriptor")]
		private static extern LibUsb.Error GetDeviceDescriptor ([In] LibUsb.Profile deviceProfileHandle, [Out] byte[] InfoData);
		
		#endregion
	}
}
